Aller au contenu principal

Programmation Asynchrone en Dart

Introduction à l'asynchronisme

Dart est un langage à thread unique, mais il permet d'effectuer des opérations asynchrones sans bloquer le thread principal. Cela est essentiel pour :

  • Requêtes réseau : Récupérer des données depuis une API
  • Opérations de fichiers : Lire/écrire des fichiers
  • Opérations de base de données : Requêtes SQL
  • Délais et timers : Attendre un certain temps
  • Interactions utilisateur : Ne pas bloquer l'interface pendant les traitements

Les Future

Un Future représente le résultat d'une opération asynchrone qui peut ne pas être disponible immédiatement. Il peut être dans l'un des états suivants :

  • Incomplet : L'opération n'est pas encore terminée
  • Complété avec succès : L'opération est terminée et a produit un résultat
  • Complété avec erreur : L'opération a échoué et a produit une erreur

Syntaxe de base

// Future qui retourne une String
Future<String> obtenirDonnees() {
return Future.delayed(
Duration(seconds: 2),
() => "Données récupérées !",
);
}

// Utilisation avec then()
void main() {
print("Début");

obtenirDonnees().then((resultat) {
print(resultat);
});

print("Fin"); // S'exécute avant le résultat du Future
}

Mots-clés async et await

Les mots-clés async et await rendent le code asynchrone plus lisible et plus facile à comprendre.

async

  • Déclaration : Utilisé pour déclarer une fonction comme asynchrone
  • Effet : La fonction retourne automatiquement un Future
  • Utilisation : Permet d'utiliser le mot-clé await à l'intérieur de la fonction
Future<String> obtenirDonnees() async {
// Code asynchrone
return "Données";
}

await

  • Attente : Suspend l'exécution jusqu'à ce que le Future soit complété
  • Simplification : Élimine les callbacks imbriqués
  • Lisibilité : Le code ressemble à du code synchrone
Future<String> obtenirDonnees() async {
// Simule une opération asynchrone (par exemple, une requête réseau)
await Future.delayed(Duration(seconds: 2));
return "Données récupérées !";
}

void main() async {
print("Début du programme");

String donnees = await obtenirDonnees(); // Attente du résultat du Future
print(donnees);

print("Fin du programme");
}

Règle importante

Le mot-clé await ne peut être utilisé qu'à l'intérieur d'une fonction marquée async.

Gestion des erreurs

Il est crucial de gérer les erreurs potentielles lors des opérations asynchrones.

Avec async/await

Future<String> obtenirDonnees() async {
try {
await Future.delayed(Duration(seconds: 2));

// Simule une erreur
if (DateTime.now().second % 2 == 0) {
throw Exception("Erreur réseau");
}

return "Données récupérées !";
} catch (e) {
print("Erreur : $e");
return "Erreur lors de la récupération des données";
}
}

Avec then() et catchError()

void main() {
obtenirDonnees()
.then((resultat) {
print(resultat);
})
.catchError((erreur) {
print("Erreur capturée : $erreur");
});
}

Bloc finally

Pour exécuter du code que l'opération réussisse ou échoue :

Future<String> obtenirDonnees() async {
try {
await Future.delayed(Duration(seconds: 2));
return "Données récupérées !";
} catch (e) {
print("Erreur : $e");
rethrow; // Relance l'erreur
} finally {
print("Opération terminée"); // Toujours exécuté
}
}

Opérations parallèles

Future.wait()

Permet d'exécuter plusieurs Future en parallèle et d'attendre que tous soient complétés :

Future<void> chargerDonnees() async {
final resultats = await Future.wait([
obtenirUtilisateur(),
obtenirProduits(),
obtenirParametres(),
]);

print("Utilisateur: ${resultats[0]}");
print("Produits: ${resultats[1]}");
print("Paramètres: ${resultats[2]}");
}

Future<String> obtenirUtilisateur() async {
await Future.delayed(Duration(seconds: 1));
return "John Doe";
}

Future<List<String>> obtenirProduits() async {
await Future.delayed(Duration(seconds: 2));
return ["Produit 1", "Produit 2"];
}

Future<Map<String, dynamic>> obtenirParametres() async {
await Future.delayed(Duration(milliseconds: 500));
return {"theme": "dark", "langue": "fr"};
}

Future.any()

Retourne le premier Future qui se complète avec succès :

Future<String> premierServeur() async {
return await Future.any([
obtenirDepuisServeur1(),
obtenirDepuisServeur2(),
obtenirDepuisServeur3(),
]);
}

Timeouts

Limiter le temps d'attente d'une opération asynchrone :

Future<String> obtenirDonneesAvecTimeout() async {
try {
final resultat = await obtenirDonnees().timeout(
Duration(seconds: 5),
onTimeout: () {
throw TimeoutException("La requête a pris trop de temps");
},
);
return resultat;
} on TimeoutException catch (e) {
print("Timeout: $e");
return "Données par défaut";
}
}

Completer

Un Completer permet de créer et contrôler manuellement un Future :

import 'dart:async';

Future<String> operationComplexe() {
final completer = Completer<String>();

// Simule une opération asynchrone
Timer(Duration(seconds: 2), () {
if (DateTime.now().second % 2 == 0) {
completer.complete("Succès");
} else {
completer.completeError("Échec");
}
});

return completer.future;
}

Exemple complet : Chargement de données

class DataService {
Future<UserData> chargerDonneesUtilisateur(int userId) async {
try {
// Étape 1 : Charger les informations de base
print("Chargement du profil...");
final profil = await _chargerProfil(userId);

// Étape 2 : Charger les détails en parallèle
print("Chargement des détails...");
final resultats = await Future.wait([
_chargerCommandes(userId),
_chargerPreferences(userId),
]);

// Étape 3 : Assembler les données
return UserData(
profil: profil,
commandes: resultats[0] as List<String>,
preferences: resultats[1] as Map<String, dynamic>,
);

} catch (e) {
print("Erreur lors du chargement : $e");
rethrow;
}
}

Future<Map<String, String>> _chargerProfil(int userId) async {
await Future.delayed(Duration(seconds: 1));
return {"nom": "John Doe", "email": "john@example.com"};
}

Future<List<String>> _chargerCommandes(int userId) async {
await Future.delayed(Duration(milliseconds: 800));
return ["Commande #123", "Commande #456"];
}

Future<Map<String, dynamic>> _chargerPreferences(int userId) async {
await Future.delayed(Duration(milliseconds: 600));
return {"theme": "dark", "notifications": true};
}
}

class UserData {
final Map<String, String> profil;
final List<String> commandes;
final Map<String, dynamic> preferences;

UserData({
required this.profil,
required this.commandes,
required this.preferences,
});
}

// Utilisation
void main() async {
final service = DataService();

try {
final donnees = await service.chargerDonneesUtilisateur(1);
print("Profil: ${donnees.profil}");
print("Commandes: ${donnees.commandes}");
print("Préférences: ${donnees.preferences}");
} catch (e) {
print("Impossible de charger les données");
}
}

Bonnes pratiques

  1. Toujours gérer les erreurs : Utilisez try-catch ou catchError
  2. Éviter les await inutiles : N'attendez que lorsque vous avez besoin du résultat
  3. Paralléliser quand possible : Utilisez Future.wait() pour les opérations indépendantes
  4. Utiliser des timeouts : Évitez les attentes infinies
  5. Retourner des Future typés : Future<String> plutôt que Future<dynamic>
  6. Ne pas oublier async : Si vous utilisez await, la fonction doit être async
  7. Attention à la performance : Trop d'opérations asynchrones peuvent ralentir l'application

Points à retenir

  • Future : Représente une valeur future (une seule valeur)
  • async : Marque une fonction comme asynchrone
  • await : Attend le résultat d'un Future
  • try-catch : Gère les erreurs asynchrones
  • Future.wait() : Exécute plusieurs Future en parallèle
  • timeout() : Limite le temps d'attente
  • Completer : Crée des Future personnalisés